using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;

namespace AxeFrog.Data
{
	public static class RepositoryHelper
	{
		private static string ConcatSelectFields(IEnumerable<string> fields)
		{
			return fields.Concat(", ", s => string.Concat("[", s, "]"));
		}

		public static SqlSelectStatement CreateSelectStatement<T>(string[] selectFields, string[] conditionFields)
		{
			return new SqlSelectStatement
			{
				SelectFields = selectFields.Concat(", ", s => string.Concat("[", s, "]")),
				From = typeof(T).Name,
				Where = conditionFields.Concat(" AND ", (s => "[" + s + "] = @" + s))
			};
		}

		public static SqlSelectStatement CreateSelectStatement<T>()
		{
			var fields = EnumerateAttributedFields<T>();
			return new SqlSelectStatement(fields.AllFields, string.Concat("[", typeof(T).Name, "]"), new[] { "ID" }, fields.OrderBy);
		}

		public static SqlSelectStatement CreateSelectStatement<T>(string whereCondition)
		{
			var fields = EnumerateAttributedFields<T>();
			return new SqlSelectStatement(fields.AllFields, string.Concat("[", typeof(T).Name, "]"), whereCondition, fields.OrderBy);
		}

		public static SqlSelectStatement CreateSelectStatement<T>(string whereCondition, string orderBy)
		{
			var fields = EnumerateAttributedFields<T>();
			return new SqlSelectStatement(fields.AllFields, string.Concat("[", typeof(T).Name, "]"), whereCondition, orderBy);
		}

		public static SqlSelectStatement CreateSelectStatement<T>(string selectFields, string whereCondition, string orderBy)
		{
			var fields = EnumerateAttributedFields<T>();
			return new SqlSelectStatement(selectFields, string.Concat("[", typeof(T).Name, "]"), whereCondition, orderBy);
		}

		public static string CreateUpdateStatement<T>(string[] updateFields, string[] conditionFields)
		{
			return string.Format("UPDATE [{0}] SET {1} WHERE {2}",
								 typeof(T).Name,
								 updateFields.Concat(", ", (s => "[" + s + "] = @" + s)),
								 conditionFields.Concat(" AND ", (s => "[" + s + "] = @" + s))
				);
		}

		public static string CreateDeleteStatement<T>()
		{
			var fields = EnumerateAttributedFields<T>();
			return string.Format("DELETE FROM [{0}] WHERE [{1}] = @{1}",
								 typeof(T).Name,
								 fields.IdField);
		}

		public static string CreateUpdateStatement<T>()
		{
			var fields = EnumerateAttributedFields<T>();
			return string.Format("UPDATE [{0}] SET {1} WHERE [{2}] = @{2}",
								 typeof(T).Name,
								 fields.NonIdFields.Concat(", ", (s => string.Concat("[", s, "] = @", s))),
								 fields.IdField);
		}

		public static string CreateInsertStatement<T>()
		{
			return CreateInsertStatement<T>(EnumerateAttributedFields<T>().NonIdFields);
		}

		public static string CreateInsertStatement<T>(string[] insertFields)
		{
			return string.Format("INSERT INTO [{0}] ({1}) VALUES ({2})",
								 typeof(T).Name,
								 insertFields.Concat(", ", s => "[" + s + "]"),
								 insertFields.Concat(", ", (s => "@" + s))
				);
		}

		public class AttributedFields
		{
			public string IdField { get; set; }
			public string[] NonIdFields { get; set; }
			public string[] AllFields { get; set; }
			public PropertyInfo[] NonIdProperties { get; set; }
			public PropertyInfo[] AllProperties { get; set; }
			public PropertyInfo IdProperty { get; set; }
			public OrderByField[] OrderBy { get; set; }
		}

		public class OrderByField : IComparable
		{
			public int Rank { get; set; }
			public string Field { get; set; }
			public bool Decending { get; set; }
			public int CompareTo(object obj)
			{
				return Rank.CompareTo(obj as int? ?? 0);
			}
		}

		private static Dictionary<Type, AttributedFields> _entityFieldsMap = new Dictionary<Type, AttributedFields>();
		public static AttributedFields EnumerateAttributedFields<T>()
		{
			// lock our general cache of enumerations seeing as multiple application instances may access it at the same time
			lock(_entityFieldsMap)
			{
				// we keep a cache because it is supposed to be used immutably and there is no point enumerating the same fields multiple times
				AttributedFields flds;
				if(_entityFieldsMap.TryGetValue(typeof(T), out flds))
					return flds;

				// find all the properties of the entity type and make a list of them and their PropertyInfo objects so we can
				// quickly build up sql statements elsewhere, as well as map parameters from entities to commands
				flds = new AttributedFields();
				var nonIdFields = new List<string>();
				var allFields = new List<string>();
				var nonIdProperties = new List<PropertyInfo>();
				var allProperties = new List<PropertyInfo>();
				var orderByFields = new List<OrderByField>();

				foreach(var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty))
				{
					if(Attribute.IsDefined(property, typeof(IdentityFieldAttribute), true))
					{
						if(flds.IdField != null)
							throw new InvalidOperationException("The specified type T has more than one field with the [IdentityField] attribute");
						property.PropertyType.AssertHasDbType();
						flds.IdField = property.Name;
						flds.IdProperty = property;
						allFields.Add(property.Name);
						allProperties.Add(property);
						var attr = (IFieldAttribute)Attribute.GetCustomAttribute(property, typeof(IdentityFieldAttribute));
						if(attr.OrderByRank.HasValue)
							orderByFields.Add(new OrderByField { Field = property.Name, Rank = attr.OrderByRank.Value, Decending = attr.Descending });
						continue;
					}
					if(Attribute.IsDefined(property, typeof(FieldAttribute), true))
					{
						property.PropertyType.AssertHasDbType();
						nonIdFields.Add(property.Name);
						nonIdProperties.Add(property);
						allFields.Add(property.Name);
						allProperties.Add(property);
						var attr = (IFieldAttribute)Attribute.GetCustomAttribute(property, typeof(FieldAttribute));
						if(attr.OrderByRank.HasValue)
							orderByFields.Add(new OrderByField { Field = property.Name, Rank = attr.OrderByRank.Value, Decending = attr.Descending });
					}
				}

				if(orderByFields.Count == 0)
					orderByFields.Add(new OrderByField { Field = "ID", Rank = 0, Decending = true });

				orderByFields.Sort((a, b) => a.Rank.CompareTo(b.Rank));
				flds.OrderBy = orderByFields.ToArray();

				if(flds.IdField == null)
					throw new InvalidOperationException("The specified type T is missing a property marked with the [IdentityField] attribute");
				if(nonIdFields.Count == 0)
					throw new InvalidOperationException("The specified type T is missing at least one property marked with the [Field] attribute");

				flds.AllFields = allFields.ToArray();
				flds.NonIdFields = nonIdFields.ToArray();
				flds.AllProperties = allProperties.ToArray();
				flds.NonIdProperties = nonIdProperties.ToArray();

				_entityFieldsMap.Add(typeof(T), flds);
				return flds;
			}
		}
	}
}